﻿using CrawlerFramework.ConfigEntity;
using CrawlerFramework.DataConverter;
using CrawlerFramework.Models;
using CrawlerFramework.Repository;
using MediaBot.Entities;
using MediaBot.Library;
using RakutenProxy.AttributeParser;
using RakutenProxy.ConfigEntity;
using RakutenProxy.DataConverter;
using RakutenProxy.Model.Rakuten;
using RakutenProxy.Repository;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;

namespace RakutenProxy.PageProcessor
{
    internal class RakutenProductDetailPageProcessor : CrawlerFramework.PageProcessor.PageProcessor
    {
        private IProductAttributeParser GetProductAttributeParser(string html)
        {
            return new RakutenProductAttributeParser(html);
        }

        private IProductAttributeParser GetProductAttributeParser(XmlNode xmlNode)
        {
            return new RakutenProductAttributeParser(xmlNode);
        }

        private void DeleteProductIfCannotGetDetail(Product product, BaseRakutenRepository<Product> productRepo, IDataConverter converter)
        {
            var jobRepo = new ScheduledJobRepository();
            var job = jobRepo.Single(j => j.LinkConfigGuid == LinkConfig.Guid && j.LinkId == Link.LinkId);
            jobRepo.Delete(job);

            var linkRepo = new LinkRepository();
            linkRepo.Delete(Link);

            //remove mapping
            DeleteConvertedData(product.ProductId, converter);

            productRepo.Delete(product);

            productRepo.Save();
            jobRepo.Save();
            linkRepo.Save();
        }

        public override CrawlerFramework.Models.CrawlStatus Execute()
        {
            Logger.Info("Start with url: " + Link.Url);

            if (!ShouldExecute())
                return CrawlStatus.Paused;

            var productRepo = new BaseRakutenRepository<Product>(true);
            var productAttributeRepo = new BaseRakutenRepository<ProductAttribute>();
            var productAttributeValueRepo = new BaseRakutenRepository<ProductAttributeValue>();
            var attributeMappingRepo = new BaseRakutenRepository<Product_ProductAttribute_Mapping>();
            var manufacturerRepo = new BaseRakutenRepository<Manufacturer>();
            var attributeCombinationRepo = new BaseRakutenRepository<ProductAttributeCombination>();

            try
            {
                if (Link.RelatedEntityName != typeof(Product).Name || Link.RelatedEntityId == null)
                    throw new InvalidOperationException("Crawling insufficient link of id: " + Link.LinkId);

                var product = productRepo.Single(p => p.ProductId == Link.RelatedEntityId);

                if (product == null)
                    throw new InvalidOperationException(string.Format("The productId = {0} is not found", Link.RelatedEntityId));

                var entityConfigPath = LinkConfig.BuildEntityConfigPath();
                var xmlSerializer = new XmlSerializer<RakutenProductDetailConfig>();
                var config = xmlSerializer.Deserialize(entityConfigPath);

                var namespaces = Utils.GetDefaultNamespaces();

                var webPage = new XmlWebPage(new Uri(Link.Url), namespaces);
                webPage.Download();

                var nsmng = webPage.XmlNamespaceManager;

                var productNode = webPage.SelectSingleNode(config.ProductNode.Context, nsmng);

                var priceStr = MatchHelper.GetString(config.Price, productNode, nsmng);
                var manufacturerName = MatchHelper.GetString(config.ManufacturerName, productNode, nsmng);
                var manufacturerUrl = MatchHelper.GetString(config.ManufacturerUrl, productNode, nsmng);
                var detailDescription = MatchHelper.GetString(config.DetailDescription, productNode, nsmng);
                var purchaseHtmlFormXml = MatchHelper.GetXmlNode(config.PurchaseHtmlForm, productNode, nsmng);

                var productConverter = RakutenDataConverterFactory.GetDataConverter(typeof(Product));
                // Delete product if cannot get the details
                if (purchaseHtmlFormXml == null)
                {
                    DeleteProductIfCannotGetDetail(product, productRepo, productConverter);
                    return CrawlStatus.CompletedWithError;
                }

                var price = Utils.GetDecimalFromString(priceStr);
                if (price > 0 && price != product.Price)
                    product.Price = price;

                var manufacturer = manufacturerRepo.Single(m => m.ManufacturerUrl == manufacturerUrl);
                if (manufacturer != null)
                {
                    manufacturer.ManufacturerName = manufacturerName;
                    product.ManufacturerId = manufacturer.ManufacturerId;
                }
                else if (!string.IsNullOrEmpty(manufacturerName))
                {
                    manufacturer = new Manufacturer { ManufacturerName = manufacturerName, ManufacturerUrl = manufacturerUrl };
                    manufacturerRepo.Add(manufacturer);
                    manufacturerRepo.Save();
                    product.ManufacturerId = manufacturer.ManufacturerId;

                    // Port from rakuten manufacturer to nopCommerce manufacturer
                    ConvertData(manufacturer.ManufacturerId, DataConverter.RakutenDataConverterFactory.GetDataConverter(typeof(Manufacturer)));
                }

                product.DetailDescription = detailDescription;

                var attributeParser = GetProductAttributeParser(purchaseHtmlFormXml);

                var purchaseActionUrl = attributeParser.GetPurchaseActionUrl();
                var purchaseFormFormat = attributeParser.GetPurchaseFormDataFormat();
                var allowedQuantities = attributeParser.GetAllowedQuantities();

                product.PurchaseActionUrl = purchaseActionUrl;
                product.PurchaseFormFormat = purchaseFormFormat;
                product.AllowedQuantity = allowedQuantities;

                var attributeCombination = attributeParser.GetProductAttributes();
                var productAttributes = new List<ProductAttribute>();

                if (attributeCombination.AttributeX != null)
                {
                    productAttributes.Add(GetProductAttribute(attributeCombination.AttributeX));
                }

                if (attributeCombination.AttributeY != null)
                {
                    productAttributes.Add(GetProductAttribute(attributeCombination.AttributeY));
                }

                foreach (var attribute in attributeCombination.OtherAttributes)
                {
                    productAttributes.Add(GetProductAttribute(attribute));
                }

                #region add the attributes to the database

                var attributeConverter = RakutenDataConverterFactory.GetDataConverter(typeof(ProductAttribute));
                var attributeValueConverter = RakutenDataConverterFactory.GetDataConverter(typeof(ProductAttributeValue));
                var attributeCombinationConverter = RakutenDataConverterFactory.GetDataConverter(typeof(ProductAttributeCombination));
                var product_attribute_mappingConverter = RakutenDataConverterFactory.GetDataConverter(typeof(Product_ProductAttribute_Mapping));

                if (product.Product_ProductAttribute_Mappings.Count == 0)   // There's no any attribute yet
                {
                    foreach (var productAttribute in productAttributes)
                    {
                        productAttribute.Product_ProductAttribute_Mappings.Add(new Product_ProductAttribute_Mapping { ControlTypeId = ControlType.DropdownList, ProductId = product.ProductId });

                        productAttributeRepo.Add(productAttribute);
                        productAttributeRepo.Save();
                    
                        ConvertData(productAttribute.ProductAttributeId, attributeConverter);
                        ConvertData(productAttribute.Product_ProductAttribute_Mappings.Select(a => a.Id), product_attribute_mappingConverter);
                        ConvertData(productAttribute.ProductAttributeValues.Select(a => a.Id), attributeValueConverter);                        
                    }
                }
                else
                {
                    var tmpProductAttributes = new List<ProductAttribute>();

                    foreach (var attributeMapping in product.Product_ProductAttribute_Mappings)
                    {
                        var attribute = attributeMapping.ProductAttribute;                                          // The attribute stored in the db
                        var gotAttribute = productAttributes.FirstOrDefault(a => a.Name.Equals(attribute.Name));    // The attribute has just crawled
                        if (gotAttribute != null)
                        {
                            if (attribute.ProductAttributeValues.Count > gotAttribute.ProductAttributeValues.Count) // some attribute values has been removed
                            {
                                var newActualValues = gotAttribute.ProductAttributeValues.Select(v => v.FormValue);
                                var deletedValues = attribute.ProductAttributeValues.Where(v => !newActualValues.Contains(v.FormValue));
                                foreach (var deletedValue in deletedValues)
                                {
                                    productAttributeValueRepo.Delete(deletedValue);
                                }
                                productAttributeValueRepo.Save();
                            }
                            else if (attribute.ProductAttributeValues.Count < gotAttribute.ProductAttributeValues.Count) // some attribute values has been added
                            {
                                var insertedActualValues = attribute.ProductAttributeValues.Select(v => v.FormValue);
                                var addedValues = gotAttribute.ProductAttributeValues.Where(v => !insertedActualValues.Contains(v.FormValue));
                                foreach (var addedValue in addedValues)
                                {
                                    addedValue.ProductAttributeId = attribute.ProductAttributeId;
                                    productAttributeValueRepo.Add(addedValue);
                                }
                                productAttributeValueRepo.Save();
                            }
                            productAttributes.Remove(gotAttribute);
                            tmpProductAttributes.Add(attribute);
                        }                        
                    }

                    // Add all remain attribute(s) that have not been inserted yet
                    foreach (var productAttribute in productAttributes)
                    {
                        productAttribute.Product_ProductAttribute_Mappings.Add(new Product_ProductAttribute_Mapping { ControlTypeId = ControlType.DropdownList, ProductId = product.ProductId });

                        productAttributeRepo.Add(productAttribute);
                        tmpProductAttributes.Add(productAttribute);

                        productAttributeValueRepo.Save();

                        ConvertData(productAttribute.ProductAttributeId, attributeConverter);
                        ConvertData(productAttribute.ProductAttributeValues.Select(a => a.Id), attributeValueConverter);
                        ConvertData(productAttribute.Product_ProductAttribute_Mappings.Select(a => a.Id), product_attribute_mappingConverter);
                    }


                    productAttributes = null;
                    productAttributes = tmpProductAttributes;
                }

                #endregion

                #region Add attribute combinations

                ProductAttribute attributeX = null, attributeY = null;
                if (attributeCombination.AttributeX != null)
                {
                    attributeX = productAttributes.First(a => a.Name == attributeCombination.AttributeX.AttributeName);
                    productAttributes.Remove(attributeX);
                }
                if (attributeCombination.AttributeY != null)
                {
                    attributeY = productAttributes.First(a => a.Name == attributeCombination.AttributeY.AttributeName);
                    productAttributes.Remove(attributeY);
                }

                var combinationRecords = attributeCombination.GetProductCombinationRecords(product.ProductId, attributeX, attributeY);

                var list = product.ProductAttributeCombinations.ToList();
                for (int i = 0; i < list.Count; i++)
                {
                    var id = list[i].Id;
                    var oldCombination = attributeCombinationRepo.Single(o => o.Id == id);
                    attributeCombinationRepo.Delete(oldCombination);
                    
                    attributeCombinationConverter.RemoveOneRecord(oldCombination.Id);

                    attributeCombinationRepo.Save();   
                }
                

                foreach (var combination in combinationRecords)
                {
                    attributeCombinationRepo.Add(combination);
                }

                productRepo.Save();
                attributeCombinationRepo.Save();

                attributeCombinationConverter.MigrateManyRecords(combinationRecords.Select(c => c.Id));

                #endregion

                ConvertData(product.ProductId, productConverter);
            }
            catch (Exception e)
            {
                var method = System.Reflection.MethodBase.GetCurrentMethod();
                var exceptionOnMethod = string.Format("{0}.{1}", method.ReflectedType, method.Name);

                var errorMessage = Utils.BuildErrorMessage(method, LinkConfig.Guid, Link.LinkId);
                Logger.Error(errorMessage, e);

                Link.EndExecution(CrawlStatus.CompletedWithError);
                return CrawlStatus.CompletedWithError;
            }
            finally
            {                
                
                productRepo.Dispose();
                productAttributeRepo.Dispose();
                productAttributeValueRepo.Dispose();
                attributeMappingRepo.Dispose();
                manufacturerRepo.Dispose();
                attributeCombinationRepo.Dispose();

                if (DoneEvent != null) DoneEvent.Set();

                Logger.Info("Finish link: " + Link.Url);
            }

            Link.EndExecution(CrawlStatus.Completed);

            return CrawlStatus.Completed;
        }

        private ProductAttribute GetProductAttribute(AttributeParser.Attribute attribute)
        {
            if (attribute == null)
                return null;

            var productAttribute = new ProductAttribute { IdentityName = attribute.HtmlName, Name = attribute.AttributeName };
            foreach (var value in attribute.AttributeValues)
            {
                var attributeValue = new ProductAttributeValue { Name = value.Name, FormValue = value.ActualValue };
                productAttribute.ProductAttributeValues.Add(attributeValue);
            }

            return productAttribute;
        }

        public RakutenProductDetailPageProcessor(LinkConfig linkConfig, Link link)
            : base(linkConfig, link)
        {
        }
    }
}